﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Djs.Tools.Tresor.Data
{
    /// <summary>
    /// Třída konvertuje pole stringů do pole byte.
    /// Pole stringů může být dvojrozměrné (charakter tabulky: řádky, sloupce) s nestejným počtem sloupců.
    /// Pole Byte se používá pro šifrování / dešifrování.
    /// </summary>
    public static class Strings
    {
        /// <summary>
        /// Vytvoří a vrátí memory stream, do něhož zapíše dodané pole stringů.
        /// </summary>
        /// <param name="stringses"></param>
        /// <returns></returns>
        public static System.IO.MemoryStream CreateStream(IEnumerable<IEnumerable<string>> stringses)
        {
            System.IO.MemoryStream ms = new System.IO.MemoryStream();                // Není using, protože jde o výstupní objekt. Using ať si dá volající metoda.
            WriteToStream(stringses, ms);
            return ms;
        }
        /// <summary>
        /// Dané pole stringů (tabulka: řádky a sloupce) vepíše do daného streamu.
        /// </summary>
        /// <param name="stringses"></param>
        /// <param name="stream"></param>
        public static void WriteToStream(IEnumerable<IEnumerable<string>> stringses, System.IO.Stream stream)
        {
            byte[] signature = Encoding.ASCII.GetBytes(SIGNATURE);
            stream.Write(signature, 0, signature.Length);

            if (stringses == null)
            {
                stream.WriteByte(NULL);
                return;
            }

            Encoding enc = Encoding.UTF8;
            foreach (IEnumerable<string> row in stringses)
            {
                if (row == null)
                {   // 0xC0
                    stream.WriteByte(NULL_ROW);
                    continue;
                }
                stream.WriteByte(NEWLINE);

                foreach (string column in row)
                {
                    if (column == null)
                    {   // 0x40
                        stream.WriteByte(NULL_COLUMN);
                        continue;
                    }

                    // Délka a kontrola délky:
                    int length = column.Length;
                    if (length >= LENGTH_MAXIMAL)
                        throw new ArgumentOutOfRangeException("stringses", "Délka stringu na vstupu přesahuje maximální povolenou hodnotu (16 M).");
                    
                    // Zkratka pro string délky 0:
                    if (length == 0)
                    {
                        stream.WriteByte((byte)0);
                        continue;
                    }

                    // String do byte bufferu, určíme délku bufferu (=počet byte, které budeme zapisovat do streamu):
                    byte[] buffer = enc.GetBytes(column);
                    length = buffer.Length;
                    if (length >= LENGTH_MAXIMAL)
                        throw new ArgumentOutOfRangeException("stringses", "Délka dat jednoho stringu na vstupu přesahuje maximální povolenou hodnotu (16 M).");

                    // Sestavím první byte (počet byte délky, dolní 4 bity délky):
                    byte data = (byte)0;
                    int lbytes = (((length & LENGTH_MASK3) != 0) ? 3 :
                                 (((length & LENGTH_MASK2) != 0) ? 2 :
                                 (((length & LENGTH_MASK1) != 0) ? 1 :
                                 (((length & LENGTH_MASK0) != 0) ? 0 : 0))));

                    // Úvodní byte: obsahuje počet byte které nesou délku stringu (0 až 3) v bitech 4 a 5, a v bitech 0 až 3 nese dolní 4 bity délky:
                    data = (byte)(data | ((lbytes << 4) & LENGTH_COUNT) | (length & LENGTH_MASK0));
                    stream.WriteByte(data);

                    // Zkratka pro string délky 0 (sem ti asi nespadne :-) ):
                    if (length == 0)
                        continue;

                    // Následující (žádný až dva) byte obsahují vyšší bity délky:
                    length = length >> 4;
                    for (int n = 0; n < lbytes; n++)
                    {
                        data = (byte)(length & LENGTH_ALL);
                        stream.WriteByte(data);
                        length = length >> 8;
                    }

                    // Následující byte nesou vlastní string v Encodin
                    stream.Write(buffer, 0, buffer.Length);
                }
            }
        }
        /// <summary>
        /// Načte ze streamu tabulku stringů, a vrátí ji.
        /// Vyhodí chybu, pokud stream je NULL nebo neobsahuje správný obsah.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public static IEnumerable<IEnumerable<string>> ReadFromStream(System.IO.Stream stream)
        {
            if (stream == null)
                throw new ArgumentNullException("stream", "Nelze číst data ze streamu, který je NULL.");

            // Signatura a úvodní byte:
            int data = _ReadSignature(stream);

            // Celá tabulka má být NULL?
            if (data == NULL) return null;

            // Výsledek tedy není NULL, začneme číst pole:
            List<List<string>> stringses = new List<List<string>>();
            while (data >= 0)
            {
                data = _ReadOneRowTest(stream, data, stringses);
            }

            return stringses;
        }
        private static int _ReadSignature(System.IO.Stream stream)
        {
            byte[] buffer = new byte[3];
            int size = stream.Read(buffer, 0, SIGNATURE.Length);
            if (size != 3)
                throw new ArgumentException("Stream neobsahuje správný obsah, jeho signatura je kratší než má být.", "stream");
            string signature = Encoding.ASCII.GetString(buffer);
            if (signature != SIGNATURE)
                throw new ArgumentException("Stream neobsahuje správný obsah, jeho signatura je jiná než má být.", "stream");

            // Úvodní byte (obsahuje buď NULL, nebo NULL_ROW, nebo NEWLINE:
            int data = stream.ReadByte();
            if (data < 0)
                throw new ArgumentException("Stream neobsahuje správný obsah, jeho data jsou kratší než mají být.", "stream");

            return data;
        }
        private static int _ReadOneRowTest(System.IO.Stream stream, int rowMark, List<List<string>> stringses)
        {
            if (rowMark < 0)
                throw new ArgumentException("Chyba řízení: nelze číst obsah řádku, jehož značka je záporná.", "rowMark");

            if (rowMark == NULL_ROW)
            {   // Následující řádek je NULL:
                stringses.Add(null);
                return stream.ReadByte();
            }

            if (rowMark == NEWLINE)
            {   // Následující řádek obsahuje data:
                List<string> row = new List<string>();
                rowMark = _ReadOneRowData(stream, row);
                stringses.Add(row);
                return rowMark;
            }
            throw new ArgumentException("Stream neobsahuje správný obsah, úvodní byte řádku má nesprávnou hodnotu.", "rowMark");
        }
        private static int _ReadOneRowData(System.IO.Stream stream, List<string> row)
        {
            Encoding enc = Encoding.UTF8;
            int data;
            while (true)
            {
                data = stream.ReadByte();                            // Úvodní byte jednoho sloupce. Anebo značka dalšího řádku.

                if (data < 0) return -1;                             // Konec souboru

                if ((data & NEWLINE) != 0) return data;              // Tady začíná další řádek, to si řeší nadřízené metody. Byte, který jsme načetli, jim vrátíme ať si to přeberou.

                if ((data & NULL) != 0)
                {   // Tento sloupec je NULL, nemusím to víc řešit:
                    row.Add(null);
                    continue;
                }

                // Zkratka pro string délky 0:
                if (data == 0)
                {
                    row.Add("");
                    continue;
                }

                // Zde máme string nenulové délky, zjistím kolik Byte je pro něj uloženo ve streamu:
                int lbytes = ((data & LENGTH_COUNT) >> 4);
                int length = (data & LENGTH_LOW);

                int shift = 4;
                for (int n = 0; n < lbytes; n++)
                {
                    int d = stream.ReadByte();
                    if (d < 0)
                        throw new ArgumentException("Stream neobsahuje správný obsah, data neočekávaně skončila.", "stream");
                    length |= ((d & LENGTH_ALL) << shift);
                    shift += 8;
                }

                // Kontrola délky:
                if (length >= LENGTH_MAXIMAL)
                    throw new ArgumentException("Stream neobsahuje správný obsah, velikost dat jednotlivého stringu přesahuje maximální povolenou hodnotu (16 M).");

                // Načteme data do bufferu v dané délce a převedeme na string:
                byte[] buffer = new byte[length];
                int size = stream.Read(buffer, 0, length);
                if (size != length)
                    throw new ArgumentException("Stream neobsahuje správný obsah, data neočekávaně skončila.", "stream");

                row.Add(enc.GetString(buffer));
            }
        }
        #region privátní konstanty a informace
        /// <summary>SAr Signatura byte[] streamu</summary>
        private const string SIGNATURE = "SAr";
        /// <summary>0x80 Bit uvádějící nový řádek</summary>
        private const byte NEWLINE = 0x80;
        /// <summary>0x40 Bit reprezentující NULL hodnotu</summary>
        private const byte NULL = 0x40;
        /// <summary>0xC0 Bity uvádějící nový řádek, který je NULL</summary>
        private const byte NULL_ROW = NEWLINE | NULL;
        /// <summary>0x40 Bit uvádějící nový sloupec, který je NULL</summary>
        private const byte NULL_COLUMN = NULL;
        /// <summary>0x30 Maska bitů, které reprezentují počet byte, které nesou délku stringu. Počet může být 0-2, byte jsou v pořadí L-H. První bity jsou v úvodním byte.</summary>
        private const byte LENGTH_COUNT = 0x30;
        /// <summary>0x0F Maska dolních bitů délky stringu v úvodním byte</summary>
        private const byte LENGTH_LOW = 0x0F;
        /// <summary>0xFF Maska všech bitů délky stringu v druhém až čtvrtém byte</summary>
        private const byte LENGTH_ALL = 0x0FF;
        /// <summary>0x0000000F Maska délky stringu, pro jejíž uložení je třeba použít 0.5 Byte</summary>
        private const int LENGTH_MASK0 = 0x0000000F;
        /// <summary>0x00000FF0 Maska délky stringu, pro jejíž uložení je třeba použít 1.5 Byte</summary>
        private const int LENGTH_MASK1 = 0x00000FF0;
        /// <summary>0x000FF000 Maska délky stringu, pro jejíž uložení je třeba použít 2.5 Byte</summary>
        private const int LENGTH_MASK2 = 0x000FF000;
        /// <summary>0x0FF00000 Maska délky stringu, pro jejíž uložení je třeba použít 3.5 Byte</summary>
        private const int LENGTH_MASK3 = 0x0FF00000;
        /// <summary>0x01000000 největší délka bufferu pro jeden string (=16MB). Pokud bude na vstupu nebo na výstupu větší hodnota, dojde k chybě.</summary>
        private const int LENGTH_MAXIMAL = 0x01000000;

        /*      Konstrukce dat :
        Index   Bit    Význam
            Celý buffer:
        0-2            Signatura "SAr"  (String Array)
        3       0x80   1 = Bude nějaký řádek, nebo  0 = žádný řádek, celé pole je NULL
                0x40   1 = Celé pole je NULL, nebo  0 = pole není NULL
                         Možné kombinace hodnot jsou:
                          0x40 = celé pole je NULL, další byte není třeba načítat, rovnou končíme.
                          0x80 = máme pole, a má zde NOT NULL řádek (a za ním možná nějaké další)
                          0xC0 = máme pole, a má zde NULL řádek (a za ním možná nějaké další)
        4              Počátek prvního řádku, následující blok se opakuje pro každý řádek
            Každý jeden řádek:
        0       0x80   1 = Zde začíná nový řádek   (0 = zde je další prvek stávajícího řádku)
                0x40   1 = Tento řádek je NULL      0 = tento řádek není NULL, ale nevím kolik má prvků
                       Dolních 6 bitů zatím nemá význam.
        1              Počátek prvního prvků, záhlaví (jeden string), následující blok se opakuje pro každý prvek (od prvního do posledního):
            Každý jeden prvek (column, string):
        0       0x80   0 = zde je další prvek stávajícího řádku         (1 = Zde začíná nový řádek)
                0x40   1 = Tento prvek (string) je NULL      0 = tento prvek (string) není NULL
                0x30   Počet byte (následujících za tímto bytem), které popisují délku stringu. Může mít hodnotu 0 až 3.
                       Nejnižší 4 bity jsou uvedeny v this byte, takže krátké stringy (v délce 0 až 15 byte) mají jen jeden režijní byte.
                       Maximální délka jednoho stringu je tedy dána velikostí 28 bitů = 2na28 = 256 MByte.
                       Počet byte popisujících délku je tedy variabilní, krátké stringy mají i kratší popisovač délky.
                       Pokud je string NULL, pak jeho délka zde uváděná je 0. Rozdíl mezi NULL a "" je definován v bitu 0x40.
                0x0F   Nejnižší 4 bity délky tohoto stringu.
        1              Nižších 8 bitů popisujících délku stringu       [podle potřeby]
        2              Středních 8 bitů popisujících délku stringu     [podle potřeby]
        3              Vyšších 8 bitů popisujících délku stringu       [podle potřeby]
        4              Začátek stringu, kódován v UTF8. Počet byte je uveden v délce v jeho záhlaví.
                       Po načtení daného počtu byte (a převedení na string) se zjistí, zda existuje další byte.
                        Pokud neexistuje, pak je načteno vše.
                        Pokud existuje, testuje se jeho bit 7 (0x80): 
                          0 = následuje další string téhož řádku
                          1 = následuje další řádek
                          0 = 
        */
        #endregion
        #region Testy
        internal static void Test()
        {
            _Test1();
            _Test2();
        }

        private static void _Test1()
        {
            List<string[]> request = new List<string[]>();

            string text = @"Podle prognóz pořádající agentury CEA trh s uživatelskými drony do pěti let převýší hodnotu jedné miliardy dolarů. Není proto divu, že drony nás poryvy vzduchu osvěžovaly na výstavišti každou chvíli. Plocha jim věnovaná vzrostla od minula o 25 procent. 
Dron MaxAero bez podvěšeného příslušenství.Značka Ubsan se soustředí především na drony pro zábavu. Na snímku je model, který se vejde na dlaň, je vybaven kamerkou pro fotografování a natáčení videí. Přes svoji miniaturní velikost dokáže ve vzduchu stabilně stát i dělat lopingy. Když narazí na strop, přisaje se k němu a nehavaruje.
Drony značky Typhoon (na snímku model Q500). Ten dokáže stoupat rychlostí až 3 m/s a klesat 2 m/s. Úhlová rychlost vrtulí je 65 stupňů/sekundu. Ve vzduchu vydrží 25 minut. Hmotnost 1,7 kg. Různé způsoby uchycení a podvěšení kamer nabízí většina lepších výrobců tak, aby vyhověla požadavkům všech. Na snímku grip Typhoonu pro GoPro kamery.
Po výstavišti jich létaly desítky. Odvážnější vystavovatelé je nechávali svištět nad hlavami návštěvníků, ostatní pro ně připravili síťové klece. A další stovky dronů byly jen nehybně postaveny v expozicích, některé ještě ve fázi prototypů. To co je nyní z polystyrénu, bude u finálního produktu z uhlíkových vláken, komentoval naše pozdvižené obočí vystavovatel u společnosti FlyHawk. 
Zdroj: http://technet.idnes.cz/ces-2015-drony-ch4-/hardware.aspx?c=A150106_215744_hardware_kuz#utm_source=sph.idnes&utm_medium=richtext&utm_content=clanek-box";
            string[] vety = text.Split('.');

            foreach (string veta in vety)
            {
                string[] slova = (veta + ".").Split(new char[] { ' ', '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries);
                request.Add(slova);
            }

            string a1 = _TestToString(request);
            string a2 = _TestToString(_TestConvert(request));
            if (a1 != a2)
                throw new InvalidOperationException("Chyba A v metodě Strings.CreateStream() nebo Strings.ReadFromStream(), výstup neodpovídá vstupu.");
        }

        private static void _Test2()
        {
            List<string[]> request = new List<string[]>();
            request.Add(new string[] { "", null, "Krátký", "Středně dlouhý", "Delší než středně dlouhý, řeklo by se až opravdu dlouhý text", null });
            request.Add(null);

            string x1 = "";
            for (int q = 20; q < 512; q++)
                x1 += ((char)q).ToString();
            request.Add(new string[] { x1 });

            string a1 = _TestToString(request);
            string a2 = _TestToString(_TestConvert(request));
            if (a1 != a2)
                throw new InvalidOperationException("Chyba B v metodě Strings.CreateStream() nebo Strings.ReadFromStream(), výstup neodpovídá vstupu.");
        }
        private static IEnumerable<IEnumerable<string>> _TestConvert(IEnumerable<IEnumerable<string>> array)
        {
            byte[] buffer = null;
            IEnumerable<IEnumerable<string>> result = null;
            using (System.IO.MemoryStream ms = CreateStream(array))
            {
                buffer = ms.ToArray();

                ms.Seek(0L, System.IO.SeekOrigin.Begin);
                result = ReadFromStream(ms);
            }
            return result;
        }
        private static string _TestToString(IEnumerable<IEnumerable<string>> array)
        {
            if (array == null) return _TEST_NULL;

            string eol = Environment.NewLine;
            string result = "";
            foreach (IEnumerable<string> veta in array)
            {
                if (veta == null)
                    result += _TEST_NULL;
                else
                {
                    string space = "";
                    foreach (string slovo in veta)
                    {
                        result += space + (slovo == null ? _TEST_NULL : slovo);
                        space = " ";
                    }
                }
                result += eol;
            }
            return result;
        }
        private const string _TEST_NULL = "{Null}";
        #endregion
    }
}
